# coding: utf-8
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtWidgets import QWidget, QVBoxLayout, QStackedWidget, QApplication
import asyncio # 新增
import requests # 新增
import json # 新增
import re
import json
import time # 新增
import uuid # 新增
from qfluentwidgets import ScrollArea,TextEdit, Pivot, FluentIcon as FIF, InfoBar, PrimaryPushSettingCard, OptionsSettingCard, Dialog # 新增 Dialog
# 导入拖放控件
from app.components.custom_drop_file_widget import CustomDropSingleFileWidget, CustomDropMultiFilesWidget
from app.components.line_edit_card import LineEditSettingCard
from app.components.slider_card import SliderSettingCard
import random
from qfluentwidgets import SettingCardGroup
from ..common.style_sheet import StyleSheet
from ..common.config import cfg
from ..common.setting import API_URL
from ..common.shared_utils import generate_coze_jwt
import os
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor

class AICardInterface(QWidget):
    """ AI制卡界面 """

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.setObjectName("aiCard")
        self.selected_file_path: str | None = None # 新增：存储选择的文件路径
        
        self.vBoxLayout = QVBoxLayout(self)
        self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
        self.vBoxLayout.setSpacing(0)
        
        # 使用滚动区域作为主要容器
        self.scrollArea = ScrollArea(self)
        self.scrollArea.setWidgetResizable(True)
        self.scrollArea.setObjectName("scrollArea")
        
        # 创建滚动区域内的内容控件
        self.scrollWidget = QWidget()
        self.scrollWidget.setObjectName("scrollWidget")
        self.scrollLayout = QVBoxLayout(self.scrollWidget) # 主内容布局，Pivot 和 StackedWidget 将添加到这里
        self.scrollLayout.setContentsMargins(10, 10, 10, 10) # 给内容区域一些边距
        self.scrollLayout.setSpacing(10)


        # Pivot 和 QStackedWidget
        self.pivot = Pivot(self)
        self.stackedWidget = QStackedWidget(self)

        # 创建页面
        self.selectFilePage = QWidget(self)
        selectFileLayout = QVBoxLayout(self.selectFilePage)
        selectFileLayout.setContentsMargins(0,0,0,0) # 子页面布局通常不需要额外边距，除非有特殊需求
        self.dropSingleFileWidget = CustomDropSingleFileWidget(self.selectFilePage)
        self.dropSingleFileWidget.setFixedHeight(200)
        self.dropSingleFileWidget.setSuffixes([
            "pdf", "txt", "md"
        ])
        selectFileLayout.addWidget(self.dropSingleFileWidget) # 移除居中对齐，使其宽度占满
        selectFileLayout.addStretch(1) # 添加伸缩因子，将控件推到顶部
        self.selectFilePage.setLayout(selectFileLayout)
        # 连接单文件选择信号
        self.dropSingleFileWidget.fileDropped.connect(self._onSingleFileSelected)
        # 连接清空文件信号
        self.dropSingleFileWidget.fileCleared.connect(self._onSingleFileCleared)

        # --- 2. 选择图片页面 ---
        self.selectImagePage = QWidget(self)
        selectImageLayout = QVBoxLayout(self.selectImagePage)
        selectImageLayout.setContentsMargins(0,0,0,0)
        self.dropMultiFilesWidget = CustomDropMultiFilesWidget(self.selectImagePage)
        self.dropMultiFilesWidget.setFixedHeight(200)
        self.dropMultiFilesWidget.setSuffixes(["jpg", "jpeg", "png"]) # 注意：setSuffixes 通常不需要前导点
        selectImageLayout.addWidget(self.dropMultiFilesWidget) # 移除居中对齐，使其宽度占满
        selectImageLayout.addStretch(1) # 添加伸缩因子，将控件推到顶部
        self.selectImagePage.setLayout(selectImageLayout)
        # 连接多文件选择信号
        self.dropMultiFilesWidget.filesDropped.connect(self._onMultiFilesSelected)
        # 连接清空文件信号
        self.dropMultiFilesWidget.filesCleared.connect(self._onMultiFilesCleared)
        
        # --- 3. 输入文本页面 ---
        self.inputTextPage = QWidget(self)
        inputTextLayout = QVBoxLayout(self.inputTextPage)
        inputTextLayout.setContentsMargins(0,0,0,0)
        self.textEdit = TextEdit(self.inputTextPage) # 使用 TextEdit 而不是 QWidget
        self.textEdit.setFixedHeight(200)
        self.textEdit.setPlaceholderText("在此输入或粘贴文本...")
        inputTextLayout.addWidget(self.textEdit)
        inputTextLayout.addStretch(1) # 添加伸缩因子，将控件推到顶部
        self.inputTextPage.setLayout(inputTextLayout)
        
        # 添加子界面到 Pivot 和 StackedWidget
        # 参数: widget, objectName, text, icon
        self._addSubInterface(self.selectFilePage, 'selectFilePage', '选择文件', FIF.DOCUMENT)
        self._addSubInterface(self.selectImagePage, 'selectImagePage', '选择图片', FIF.PHOTO)
        self._addSubInterface(self.inputTextPage, 'inputTextPage', '输入文本', FIF.EDIT)

        # 将 Pivot 和 StackedWidget 添加到滚动区域的布局中
        self.scrollLayout.addWidget(self.pivot)
        self.scrollLayout.addWidget(self.stackedWidget)
        
        # 设置默认选中的项目
        self.pivot.setCurrentItem('selectFilePage')
        # 连接 Pivot 的选项变化信号到 StackedWidget 的切换槽
        self.pivot.currentItemChanged.connect(self._onCurrentItemChanged)
        
        # 设置滚动区域的内容控件
        self.scrollArea.setWidget(self.scrollWidget)
        self.vBoxLayout.addWidget(self.scrollArea)
        
        # --- 基础设置 --- 
        self.basicSettingsGroup = SettingCardGroup("基础设置", self.scrollWidget) # 将父控件改为 self.scrollWidget
        self.pageSelectCard = LineEditSettingCard(
            FIF.EDIT, 
            "页码选择", 
            "空为全部;输入1,3提取1、3页;输入5-9提取5-9页。", 
            parent=self.basicSettingsGroup
        )
        self.pageSelectCard.lineEdit.setPlaceholderText("例如: 1,3,5-9")
        self.pageSelectCard.lineEdit.textChanged.connect(self._on_page_select_text_changed)
        self.basicSettingsGroup.addSettingCard(self.pageSelectCard)

        # 卡片数量设置
        self.cardCountSlider = SliderSettingCard(
            FIF.TAG, # 或者选择一个更合适的图标
            "卡片数量",
            "每截断字符生成卡片数量(0为完整覆盖所有知识点)",
            cfg.aiCardCount, # 使用配置项
            parent=self.basicSettingsGroup,
            minimum=0,
            maximum=100
        )
        self.basicSettingsGroup.addSettingCard(self.cardCountSlider)

        # 字符截断设置
        self.charTruncationSlider = SliderSettingCard(
            FIF.CUT, # 或者选择一个更合适的图标
            "字符截断",
            "设置处理文本时的字符数上限(拆分多次AI请求避免知识遗漏)",
            cfg.charTruncationLimit, # 使用配置项
            parent=self.basicSettingsGroup,
            minimum=1000,
            maximum=10000
        )
        self.basicSettingsGroup.addSettingCard(self.charTruncationSlider)

        self.scrollLayout.addWidget(self.basicSettingsGroup) # 将设置组添加到滚动区域的布局中

        # --- 题型设置 --- 
        self.questionTypeGroup = SettingCardGroup("题型比例设置", self.scrollWidget)

        self.shortAnswerSlider = SliderSettingCard(
            FIF.QUESTION, "简答题", "直接进行提问回答", 
            cfg.shortAnswerCount, self.questionTypeGroup, 0, 10)
        self.questionTypeGroup.addSettingCard(self.shortAnswerSlider)

        self.singleChoiceSlider = SliderSettingCard(
            FIF.CHECKBOX, "单选题", "标准四选项单选题", 
            cfg.singleChoiceCount, self.questionTypeGroup, 0, 10)
        self.questionTypeGroup.addSettingCard(self.singleChoiceSlider)

        self.multipleChoiceSlider = SliderSettingCard(
            FIF.CHECKBOX, "多选题", "五选项多选题。最少选两个，最多选五个（2-5个选项随机）", 
            cfg.multipleChoiceCount, self.questionTypeGroup, 0, 10)
        self.questionTypeGroup.addSettingCard(self.multipleChoiceSlider)

        self.trueFalseSlider = SliderSettingCard(
            FIF.CANCEL, "判断题", "简单对错判断题", 
            cfg.trueFalseCount, self.questionTypeGroup, 0, 10)
        self.questionTypeGroup.addSettingCard(self.trueFalseSlider)

        self.singleFillBlankSlider = SliderSettingCard(
            FIF.EDIT, "单空填空题", "单个填空位置，填空处使用[...]填充", 
            cfg.singleFillBlankCount, self.questionTypeGroup, 0, 10)
        self.questionTypeGroup.addSettingCard(self.singleFillBlankSlider)

        self.multipleFillBlankSlider = SliderSettingCard(
            FIF.EDIT, "多空填空题", "大于1个填空位置，填空处使用[...]填充", 
            cfg.multipleFillBlankCount, self.questionTypeGroup, 0, 10)
        self.questionTypeGroup.addSettingCard(self.multipleFillBlankSlider)

        self.scrollLayout.addWidget(self.questionTypeGroup)

        # --- 自定义提示词设置 ---
        self.customPromptGroup = SettingCardGroup("自定义提示词", self.scrollWidget)
        self.customPromptTextEdit = TextEdit(self.customPromptGroup) # 使用 TextEdit
        self.customPromptTextEdit.setFixedHeight(100)
        self.customPromptTextEdit.setPlaceholderText("有什么需要交代大模型的？ (可为空)")
        self.customPromptTextEdit.setText(cfg.get(cfg.customPromptText)) # 加载保存的文本
        self.customPromptTextEdit.textChanged.connect(self._on_custom_prompt_text_changed) # 连接信号
        self.customPromptGroup.addSettingCard(self.customPromptTextEdit) # 使用 addSettingCard
        self.scrollLayout.addWidget(self.customPromptGroup)

        # --- 自定义快捷按钮设置 ---
        self.customShortcutGroup = SettingCardGroup("自定义快捷按钮", self.scrollWidget)
        self.customShortcutTextEdit = TextEdit(self.customShortcutGroup)
        self.customShortcutTextEdit.setFixedHeight(100)
        self.customShortcutTextEdit.setPlaceholderText("格式：按钮名称|提示词，每行配置一个按钮(回车换行)，最多新添加4个")
        self.customShortcutTextEdit.setText(cfg.get(cfg.customShortcutText))
        self.customShortcutTextEdit.textChanged.connect(self._on_custom_shortcut_text_changed)
        self.customShortcutGroup.addSettingCard(self.customShortcutTextEdit)

        # 新增：保存自定义快捷按钮的按钮
        self.saveCustomShortcutButton = PrimaryPushSettingCard(
            "同步",
            FIF.SYNC, # 可以选择合适的图标
            "同步",
            "点击自定义快捷按钮将同步到卡片",
            self.customShortcutGroup
        )
        self.saveCustomShortcutButton.clicked.connect(self._on_save_custom_shortcut_clicked) # 连接点击信号
        self.customShortcutGroup.addSettingCard(self.saveCustomShortcutButton)

        self.scrollLayout.addWidget(self.customShortcutGroup)

        # --- 语言朗读设置 ---
        self.languageNarrationGroup = SettingCardGroup("语言朗读设置", self.scrollWidget)
        self.roleSelectCard = OptionsSettingCard(
            cfg.aiCardNarrationRole, # 假设你有一个配置项来存储角色
            FIF.PEOPLE, # 或者选择一个更合适的图标
            "角色设置",
            "选择朗读的角色",
            texts=["男", "女"],
            parent=self.languageNarrationGroup
        )
        self.languageNarrationGroup.addSettingCard(self.roleSelectCard)

        self.narrationSpeedSlider = SliderSettingCard(
            FIF.SPEED_MEDIUM, # 或者选择一个更合适的图标
            "语速设置",
            "设置朗读的语速 (0.25 - 4.0)",
            cfg.aiCardNarrationSpeed, # 假设你有一个配置项来存储语速
            parent=self.languageNarrationGroup,
            minimum=25, # 0.25 * 100
            maximum=400 # 4.0 * 100
        )
        # 设置滑块的显示文本格式
        self.narrationSpeedSlider.slider.valueChanged.connect(
            lambda value: self.narrationSpeedSlider.valueLabel.setText(f"{value/100:.2f}")
        )
        self.narrationSpeedSlider.valueLabel.setText(f"{cfg.get(cfg.aiCardNarrationSpeed)/100:.2f}")


        self.languageNarrationGroup.addSettingCard(self.narrationSpeedSlider)
        self.scrollLayout.addWidget(self.languageNarrationGroup)


        # --- 开始制卡设置 ---
        self.startMakingCardGroup = SettingCardGroup("开始制卡", self.scrollWidget)
        
        self.deckNameCard = LineEditSettingCard(
            FIF.LABEL, "卡组名称", "请输入卡组名称",
            configItem=None, # 值不需要保存
            parent=self.startMakingCardGroup
        )
        self.startMakingCardGroup.addSettingCard(self.deckNameCard)

        self.submitTaskCard = PrimaryPushSettingCard(
            "提交任务", FIF.SAVE, "提交", "将生成的.apkg卡包发送至您的邮箱", self.startMakingCardGroup
        )
        self.startMakingCardGroup.addSettingCard(self.submitTaskCard)

        self.scrollLayout.addWidget(self.startMakingCardGroup)

        self.scrollLayout.addStretch(1) # 添加伸缩因子，将前面的内容顶上去
        
        # 连接信号
        self.textEdit.textChanged.connect(self._update_button_states)
        self.deckNameCard.lineEdit.textChanged.connect(self._update_button_states)
        self.submitTaskCard.clicked.connect(self._on_submit_task_clicked)
        
        # 应用样式表
        StyleSheet.AI_CARD_INTERFACE.apply(self)

        # 初始化按钮状态
        self._update_button_states()

        # 连接配置项变化信号以同步到服务器
        cfg.aiCardNarrationRole.valueChanged.connect(self._on_narration_settings_changed)
        # For speed, sync only when the slider is released.
        self.narrationSpeedSlider.slider.sliderReleased.connect(self._on_narration_settings_changed)

    def _addSubInterface(self, widget: QWidget, object_name: str, text: str, icon):
        widget.setObjectName(object_name)
        # widget.setAlignment(Qt.AlignmentFlag.AlignCenter) # QWidget没有setAlignment, 如果需要内容居中，需要在widget内部的布局或控件上设置
        self.stackedWidget.addWidget(widget)
        self.pivot.addItem(
            routeKey=object_name,
            text=text,
            # onClick=lambda: self.stackedWidget.setCurrentWidget(widget), # currentItemChanged 信号会处理切换
            icon=icon
        )

    def _onCurrentItemChanged(self, routeKey: str):
        # 这个槽函数用于响应 Pivot.currentItemChanged(str) 信号
        # 在较新版本的 qfluentwidgets 中，Pivot.addItem 时可以直接关联切换逻辑，
        # 或者通过 objectName 查找 widget
        widget = self.findChild(QWidget, routeKey)
        if widget:
            self.stackedWidget.setCurrentWidget(widget)

    def _onSingleFileSelected(self, file_path: str):
        """ 单个文件选择成功时的处理 """
        self.dropSingleFileWidget.setTitle(file_path)
        self.selected_file_path = file_path
        self._update_button_states()

    def _onSingleFileCleared(self):
        """ 清空单个文件时的处理 """
        self.selected_file_path = None
        self._update_button_states()

    def _onMultiFilesSelected(self, file_paths: list):
        """ 多个文件选择成功时的处理 """
        if file_paths:
            self.selected_file_path=file_paths
            #  可以只显示第一个文件名或者文件数量
            self.dropMultiFilesWidget.setTitle(f"{file_paths[0]} 等{len(file_paths)} 个文件")
            self._update_button_states()  # 添加这行来更新按钮状态

    def _onMultiFilesCleared(self):
        """ 清空多个文件时的处理 """
        self.selected_file_path = None
        self._update_button_states()

    def _validatePageRange(self, text: str, show_infobar: bool = True) -> bool:
        """ 验证页码范围输入 """
        valid_format = True # 先假设格式是正确的

        if not text.strip():
            # 空字符串是有效格式，表示处理所有页面
            pass # valid_format 保持 True
        else:
            parts = text.split(',')
            for part_idx, part_str in enumerate(parts):
                part = part_str.strip()
                if not part:
                    # 处理像 "1,,2" 这样的情况，单个空部分不视为错误，但如果整个字符串是 "," 则视为空
                    if len(parts) == 1: # 如果整个输入只是一个或多个逗号
                        if show_infobar: InfoBar.error("错误", "页码输入不能为空", duration=3000, parent=self)
                        valid_format = False
                        break
                    continue

                if '-' in part:
                    try:
                        start, end = map(int, part.split('-'))
                        if start < 1 or end < 1 or start > end:
                            if show_infobar: InfoBar.error("错误", f"无效的页码范围: {part}", duration=3000, parent=self)
                            valid_format = False
                            break
                    except ValueError:
                        if show_infobar: InfoBar.error("错误", f"无效的页码范围格式: {part}", duration=3000, parent=self)
                        valid_format = False
                        break
                else:
                    try:
                        page = int(part)
                        if page < 1:
                            if show_infobar: InfoBar.error("错误", f"无效的页码: {part}", duration=3000, parent=self)
                            valid_format = False
                            break
                    except ValueError:
                        if show_infobar: InfoBar.error("错误", f"无效的页码格式: {part}", duration=3000, parent=self)
                        valid_format = False
                        break
        
        # 移除按钮状态设置
        # self.exportApkgCard.setEnabled(valid_format)
        # self.syncAnkiCard.setEnabled(valid_format)
        
        return valid_format

    def _on_page_select_text_changed(self, text: str):
        """ 页码输入框内容改变时的处理 """
        self._validatePageRange(text, show_infobar=True) # 输入时验证并提示
        self._update_button_states()

    def _update_button_states(self):
        """ 更新导出和同步按钮的启用状态 """
        is_page_range_format_valid = self._validatePageRange(self.pageSelectCard.lineEdit.text(), show_infobar=False)

        has_file_input = bool(self.selected_file_path)
        has_text_input = bool(self.textEdit.toPlainText().strip())
        is_input_present = has_file_input or has_text_input

        # Remove deck name validation since it's now optional
        can_proceed = is_page_range_format_valid and is_input_present
        
        self.submitTaskCard.setEnabled(can_proceed)

    def collect_form_data(self) -> dict:
        """收集所有表单数据，返回字典，action为点击的按钮类型"""
        deck_name = self.deckNameCard.lineEdit.text().strip()
        if not deck_name:
            deck_name = "未命名"
            
        data = {
            "selected_file_path": self.selected_file_path,
            "input_text": self.textEdit.toPlainText().strip(),
            "deck_name": deck_name,
            "custom_prompt": self.customPromptTextEdit.toPlainText().strip(),
            "email":cfg.get(cfg.email),
            "jwt":cfg.get(cfg.jwtToken),
            "ai_card_count": cfg.get(cfg.aiCardCount),
            "char_truncation": cfg.get(cfg.charTruncationLimit), # 新增：字符截断限制
            "short_answer": cfg.get(cfg.shortAnswerCount),
            "single_choice": cfg.get(cfg.singleChoiceCount),
            "multiple_choice": cfg.get(cfg.multipleChoiceCount),
            "true_false": cfg.get(cfg.trueFalseCount),
            "single_fill_blank": cfg.get(cfg.singleFillBlankCount),
            "multiple_fill_blank": cfg.get(cfg.multipleFillBlankCount),
            "model": cfg.get(cfg.customLlmName),
            "key": cfg.get(cfg.llmApiKey),
            
        }
        return data

    def _on_submit_task_clicked(self):
        """ 点击导出Apkg按钮时的处理 """
        is_page_range_format_valid = self._validatePageRange(self.pageSelectCard.lineEdit.text(), show_infobar=True)
        if not is_page_range_format_valid:
            return

        has_file_input = bool(self.selected_file_path)
        has_text_input = bool(self.textEdit.toPlainText().strip())
        if not (has_file_input or has_text_input):
            InfoBar.error("错误", "请选择文件或输入文本内容", duration=3000, parent=self)
            return
            
        # 检查大模型名称和密钥是否为空
        model_name = cfg.get(cfg.customLlmName)
        api_key = cfg.get(cfg.llmApiKey)
        if not model_name or not api_key:
            InfoBar.error("错误", "大模型名称和API密钥不能为空（左下角设置）", duration=3000, parent=self)
            return

        # Remove deck name validation check since it's now optional
        # 检查用户当前是否有正在进行的任务
        try:
            jwt_token = cfg.get(cfg.jwtToken)
                
            headers = {
                "Authorization": f"Bearer {jwt_token}",
                "Content-Type": "application/json"
            }
            
            # 调用API检查任务状态
            response = requests.get(f"{API_URL}/task/status", headers=headers)
            
            if response.status_code == 200:
                response_data = response.json()
                if response_data.get('data', {}).get('is_processing', False):
                    InfoBar.warning("警告", "您有正在进行的任务，请等待当前任务完成后再提交新任务", 
                                   duration=5000, parent=self)
                    return
            else:
                InfoBar.error("错误", f"检查任务状态失败: {response.status_code}", 
                             duration=3000, parent=self)
                return
                
        except Exception as e:
            InfoBar.error("错误", f"检查任务状态时发生错误: {str(e)}", duration=3000, parent=self)
            return

        # 新增：显示确认对话框
        title = "任务提交确认"
        content = "请确保需求、页码、数量、题型设置正确！\n一旦提交，任务将无法中断或取消！\n若上传内容过多系统将持续处理直至余额消耗完毕！"
        dialog = Dialog(title, content, self.window()) # self.window() 获取顶层窗口
        if not dialog.exec():
            return # 如果用户点击取消，则不继续

        form_data = self.collect_form_data()
        try:
            self._submit_task(form_data)
            
            
        except Exception as e:
            InfoBar.error("错误", f"工作流执行失败: {e}", duration=5000, parent=self)

    def _on_custom_prompt_text_changed(self):
        """ 自定义提示词内容改变时的处理 """
        cfg.set(cfg.customPromptText, self.customPromptTextEdit.toPlainText())
        # self._update_button_states() # 如果提示词影响按钮状态，取消此行注释

    def _on_custom_shortcut_text_changed(self):
        """ 自定义快捷按钮内容改变时的处理 """
        cfg.set(cfg.customShortcutText, self.customShortcutTextEdit.toPlainText()) # 只更新本地配置
        # self._update_button_states() # 如果快捷按钮影响按钮状态，取消此行注释

    def _on_save_custom_shortcut_clicked(self):
        """ 点击保存自定义快捷按钮时的处理 """
        button_text = self.customShortcutTextEdit.toPlainText()
        # 调用API保存到数据库
        try:
            # 从配置中获取JWT Token
            jwt_token = cfg.get(cfg.jwtToken)
            if not jwt_token:
                InfoBar.error("错误", "用户未登录，无法保存自定义快捷按钮", duration=3000, parent=self)
                return

            headers = {
                "Authorization": f"Bearer {jwt_token}",
                "Content-Type": "application/json"
            }
            data = {"button": button_text}
            # 从配置中获取API地址，如果API地址不在配置中，您可能需要硬编码或通过其他方式获取
            # 这里假设API地址是固定的，或者您有方法获取它
     
            response = requests.post(f"{API_URL}/user/button", headers=headers, json=data)
            
            if response.status_code == 200:
                response_data = response.json()
                if response_data.get("status") == "success":
                    InfoBar.success("成功", "自定义快捷按钮已同步到卡片", duration=3000, parent=self)
                    cfg.set(cfg.customShortcutText, button_text) # 保存成功后，也更新本地配置
                else:
                    InfoBar.error("错误", f"保存失败: {response_data.get('message', '未知错误')}", duration=3000, parent=self)
            else:
                InfoBar.error("错误", f"保存失败，状态码: {response.status_code}", duration=3000, parent=self)
        except requests.exceptions.RequestException as e:
            InfoBar.error("错误", f"请求失败: {e}", duration=3000, parent=self)
        except Exception as e:
            InfoBar.error("错误", f"保存过程中发生未知错误: {e}", duration=3000, parent=self)

    def _on_narration_settings_changed(self):
        """ 语言朗读设置（角色或语速）发生变化时，同步到服务器 """
        narration_role_text = cfg.get(cfg.aiCardNarrationRole)
        narration_speed_value = cfg.get(cfg.aiCardNarrationSpeed)

        # 将角色文本转换为整数 (0 for 女, 1 for 男)
        # API /user/voice-settings 期望 voice: 0 for 女(shimmer), 1 for 男(onyx)
        role_int = 0 if narration_role_text == "女" else 1
        
        # 将语速值 (25-400) 转换为浮点数 (0.25-4.0)
        speed_float = narration_speed_value / 100.0

        jwt_token = cfg.get(cfg.jwtToken)

        headers = {
            "Authorization": f"Bearer {jwt_token}",
            "Content-Type": "application/json"
        }
        data = {
            "voice": role_int, # 对应API的 'voice' 字段
            "speed": speed_float # 对应API的 'speed' 字段
        }

        try:
            response = requests.post(f"{API_URL}/user/voice-settings", headers=headers, json=data, timeout=5)
            response.raise_for_status() # 如果状态码不是2xx，则抛出HTTPError

            response_data = response.json()
            if response_data.get("status") == "success":
                InfoBar.success("提示", "语音朗读设置已同步至云端", duration=2000, parent=self)
            else:
                api_message = response_data.get('message', '未知响应')
                if "更新语音设置成功" in api_message or "设置无变化" in api_message :
                     InfoBar.info("提示", f"语音朗读设置已更新: {api_message}", duration=2000, parent=self)
                else:
                    InfoBar.warning("提示", f"语音朗读设置同步可能未成功: {api_message}", duration=3000, parent=self)

        except requests.exceptions.RequestException as e:
            error_message = f"同步语音朗读设置请求失败: {e}"
            InfoBar.error("网络错误", error_message, duration=3000, parent=self)
        except Exception as e: # 其他Python异常
            InfoBar.error("程序错误", f"同步语音朗读设置时发生内部错误: {e}", duration=3000, parent=self)

    

    def _submit_task(self, form_data):
        """提交任务，包括文件上传和API调用，期间显示进度环，任务完成后移除进度环"""
        from app.components.processing_ring import ProcessingRing
        # 锁定按钮并设置为"提交中..."

        progress_ring = ProcessingRing(self)
        progress_ring.show()
        try:
            coze_jwt = generate_coze_jwt()
            file_path = form_data.get("selected_file_path")
            # 判断 file_path 是否为数组，如果是，则将图片列表转为 PDF 并保存到临时文件
            if isinstance(file_path, list):
                from PIL import Image
                import tempfile
                import os
                images = []
                for img_path in file_path:
                    img = Image.open(img_path)
                    if img.mode != "RGB":
                        img = img.convert("RGB")
                    images.append(img)
                if images:
                    temp_pdf = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
                    images[0].save(temp_pdf.name, save_all=True, append_images=images[1:])
                    temp_pdf.close() 
                    file_path=temp_pdf.name
            elif file_path and file_path.lower().endswith((".md",".txt")):
                with open(file_path, 'r', encoding='utf-8') as f:
                    form_data["input_text"] = f.read()
                del form_data["selected_file_path"]
            elif file_path and file_path.lower().endswith(".pdf"):
                import fitz
                import tempfile
                import os
                page_range_text = self.pageSelectCard.lineEdit.text().strip()
                if  page_range_text:
                    try:
                        doc = fitz.open(file_path)
                        new_doc = fitz.open()
                        pages = []
                        for part in page_range_text.split(','):
                            part = part.strip()
                            if '-' in part:
                                start, end = part.split('-')
                                start = int(start) - 1
                                end = int(end)
                                pages.extend(range(start, end))
                            else:
                                pages.append(int(part) - 1)
                        
                        # 检查是否有有效页面
                        valid_pages = [p for p in pages if 0 <= p < len(doc)]
                        if not valid_pages:
                            InfoBar.error("错误", "未找到有效的PDF页面，请检查页码范围", duration=3000, parent=self)
                            progress_ring.hide()
                            progress_ring.deleteLater()
                            return
                            
                        for page_num in valid_pages:
                            page = doc.load_page(page_num)
                            annotations = page.annots()
                            rect_annotations = [a for a in annotations if a.type[0] == 4]
                            if rect_annotations:
                                for annot in rect_annotations:
                                    rect = annot.rect
                                    new_page = new_doc.new_page(width=rect.width, height=rect.height)
                                    dest_rect = fitz.Rect(0, 0, rect.width, rect.height)
                                    new_page.show_pdf_page(dest_rect, doc, page_num, clip=rect)
                            else:
                                new_doc.insert_pdf(doc, from_page=page_num, to_page=page_num)
                        
                        # 确保新文档有页面
                        if new_doc.page_count == 0:
                            InfoBar.error("错误", "无法创建PDF，未找到有效内容", duration=3000, parent=self)
                            progress_ring.hide()
                            progress_ring.deleteLater()
                            doc.close()
                            new_doc.close()
                            return
                        
                        # 使用唯一文件名避免权限冲突
                        import uuid
                        temp_filename = f"temp_pdf_{uuid.uuid4().hex}.pdf"
                        temp_filepath = os.path.join(tempfile.gettempdir(), temp_filename)
                        
                        try:
                            new_doc.save(temp_filepath)
                            file_path = temp_filepath
                        except Exception as e:
                            InfoBar.error("错误", f"保存临时PDF文件时出错: {e}", duration=3000, parent=self)
                            
                        # 确保关闭文档
                        doc.close()
                        new_doc.close()
                    except Exception as e:
                        InfoBar.error("错误", f"处理PDF页面时出错: {e}", duration=3000, parent=self)
                        progress_ring.hide()
                        progress_ring.deleteLater()
                        return
            # elif file_path and file_path.lower().endswith((".mp4",".avi")):
            #     import tempfile
            #     from moviepy.video.io.VideoFileClip import VideoFileClip
            #     import os
                
            #     temp_audio_path = None
            #     try:
            #         temp_audio = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
            #         temp_audio_path = temp_audio.name
            #         temp_audio.close()
                    
            #         video = VideoFileClip(file_path)
            #         video.audio.write_audiofile(temp_audio_path)
            #         file_path = temp_audio_path
            #         video.close()

            #     except Exception as e:
            #         InfoBar.error("错误", f"处理视频文件时出错: {e}", duration=3000, parent=self)
            #         return
            if file_path:
                file_url = self._upload_file_to_oss(file_path, progress_ring)
                form_data["selected_file_path"] = file_url
            pattern = r'!\[.*?\]\(data:image\/[^)]+\)'
            form_data["input_text"] = re.sub(pattern, '', form_data["input_text"])
 
            headers = {
                "Authorization": f"Bearer {cfg.get(cfg.jwtToken)}",
                "X-Fc-Invocation-Type": "Async",
                "Content-Type": "application/json"
            }
            try:
                response = requests.post("https://ai-card-ncwwfqkafs.cn-hangzhou.fcapp.run", headers=headers, json=form_data, timeout=60)
                response.raise_for_status()
                InfoBar.success("成功", "任务提交成功，请等待处理完成。", duration=3000, parent=self)
            except requests.exceptions.RequestException as e:
                InfoBar.error("网络错误", e, duration=5000, parent=self)
            except Exception as e:
                InfoBar.error("程序错误", f"提交任务过程中发生内部错误: {e}", duration=5000, parent=self)
            finally:
                progress_ring.hide()
                progress_ring.deleteLater()
        except Exception as e:
            progress_ring.hide()
            progress_ring.deleteLater()

    def _upload_file_to_oss(self, file_path, progress_ring=None):
        """上传文件到阿里云OSS并返回文件URL，支持外部传入进度环"""
        try:
            jwt_token = cfg.get(cfg.jwtToken)
            headers = {"Authorization": f"Bearer {jwt_token}"} if jwt_token else {}
            auth_response = requests.get(f"{API_URL}/get_oss_auth", headers=headers)
            auth_response.raise_for_status()
            oss_data = auth_response.json()
            required_fields = ["host", "policy", "accessId", "signature"]
            if not all(field in oss_data for field in required_fields):
                return None
            file_ext = os.path.splitext(os.path.basename(file_path))[1]
            random_str = f"ztough-{int(time.time())}-{random.randint(1000, 9999)}{file_ext}"
            oss_file_name = f"user-uploads/{random_str}"
            form_data = {
                'key': oss_file_name,
                'policy': oss_data["policy"],
                'OSSAccessKeyId': oss_data["accessId"],
                'success_action_status': '200',
                'signature': oss_data["signature"]
            }
            if progress_ring is None:
                from app.components.processing_ring import ProcessingRing
                progress_ring = ProcessingRing(self)
                progress_ring.show()
            result = {"file_url": None, "error": None}
            class UploadThread(QThread):
                progress_updated = Signal(int)
                upload_finished = Signal(bool)
                def run(self):
                    try:
                        file_size = os.path.getsize(file_path)
                        with open(file_path, 'rb') as file_obj:
                            m = MultipartEncoder(
                                fields={
                                    **form_data,
                                    'file': (os.path.basename(file_path), file_obj, 'application/octet-stream')
                                }
                            )
                            monitor = MultipartEncoderMonitor(
                                m,
                                lambda monitor: self.progress_updated.emit(int(monitor.bytes_read * 100 / file_size))
                            )
                            headers = {'Content-Type': monitor.content_type}
                            response = requests.post(
                                oss_data["host"],
                                data=monitor,
                                headers=headers
                            )
                            if response.status_code == 200:
                                result["file_url"] = f"{oss_data['host']}/{oss_file_name}"
                                self.upload_finished.emit(True)
                            else:
                                result["error"] = f"上传文件失败，状态码: {response.status_code}"
                                self.upload_finished.emit(False)
                    except Exception as e:
                        result["error"] = f"上传文件过程中发生错误: {e}"
                        self.upload_finished.emit(False)
            upload_thread = UploadThread()
            upload_thread.progress_updated.connect(progress_ring.setValue)
            def on_upload_finished(success):
                if not success:
                    InfoBar.error("上传失败", result["error"], duration=3000, parent=self)
            upload_thread.upload_finished.connect(on_upload_finished)
            upload_thread.start()
            while upload_thread.isRunning():
                QApplication.processEvents()
            return result["file_url"]
        except Exception as e:
            return None
